/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software.
 * If the software was purchased under a paid Alfresco license, the terms of
 * the paid license agreement will prevail.  Otherwise, the software is
 * provided under the following open source license terms:
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.tagging;

import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runners.MethodSorters;

import org.alfresco.model.ContentModel;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.action.ActionModel;
import org.alfresco.repo.action.AsynchronousActionExecutionQueuePolicies;
import org.alfresco.repo.action.AsynchronousActionExecutionQueuePolicies.OnAsyncActionExecute;
import org.alfresco.repo.audit.AuditComponent;
import org.alfresco.repo.audit.AuditServiceImpl;
import org.alfresco.repo.audit.UserAuditFilter;
import org.alfresco.repo.jscript.ClasspathScriptLocation;
import org.alfresco.repo.node.NodeRefPropertyMethodInterceptor;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.action.ActionTrackingService;
import org.alfresco.service.cmr.action.ExecutionSummary;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.*;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.site.SiteVisibility;
import org.alfresco.service.cmr.tagging.TagDetails;
import org.alfresco.service.cmr.tagging.TagScope;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.BaseSpringTest;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.testing.category.LuceneTests;
import org.alfresco.util.testing.category.RedundantTests;

/**
 * Tagging service implementation unit test
 *
 * @author Roy Wetherall
 * @author Nick Burch
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TaggingServiceImplTest extends BaseSpringTest
{
    private static final Log logger = LogFactory.getLog(TaggingServiceImplTest.class);

    /** Services */
    private TaggingServiceImpl taggingService;
    private NodeService nodeService;
    private FileFolderService fileFolderService;
    private CopyService copyService;
    private CheckOutCheckInService checkOutCheckInService;
    private ScriptService scriptService;
    private AuditService auditService;
    private ActionService actionService;
    private ActionTrackingService actionTrackingService;
    private TransactionService transactionService;
    private AuthenticationComponent authenticationComponent;
    private PersonService personService;
    private PermissionService permissionService;
    private MutableAuthenticationService authenticationService;
    private AsyncOccurs asyncOccurs;
    private SiteService siteService;

    private NodeRefPropertyMethodInterceptor nodeRefPropInterceptor;

    private static StoreRef storeRef;
    private static NodeRef rootNode;
    private NodeRef folder;
    private NodeRef subFolder;
    private NodeRef document;
    private NodeRef subDocument;

    private static final String TEST_SITE_PRESET = "testSitePreset";
    private static final String TEST_TITLE = "TitleTest This is my title";
    private static final String TEST_DESCRIPTION = "DescriptionTest This is my description";

    private static final String TAG_1 = "tag one";
    private static final String TAG_2 = "tag two";
    private static final String TAG_3 = "Tag Three";
    private static final String TAG_4 = "tag four";
    private static final String TAG_5 = "tag five";
    private static final String TAG_I18N = "àâæçéèêëîïôœùûüÿñ";

    private static final String BAD_TAG = "bad \n tag";
    private static final String BAD_TAG2 = "Broken|2";

    private static final String UPPER_TAG = "House";
    private static final String LOWER_TAG = "house";

    private static boolean init = false;

    @Before
    public void setUp() throws Exception
    {
        // Detect any dangling transactions as there is a lot of direct UserTransaction manipulation
        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
        {
            throw new IllegalStateException(
                    "There should not be any transactions when starting test: " +
                            AlfrescoTransactionSupport.getTransactionId() + " started at " +
                            new Date(AlfrescoTransactionSupport.getTransactionStartTime()));
        }

        // Get services
        this.taggingService = (TaggingServiceImpl) applicationContext.getBean("taggingService");
        this.nodeService = (NodeService) applicationContext.getBean("NodeService");
        this.fileFolderService = (FileFolderService) applicationContext.getBean("FileFolderService");
        this.copyService = (CopyService) applicationContext.getBean("CopyService");
        this.checkOutCheckInService = (CheckOutCheckInService) applicationContext.getBean("CheckoutCheckinService");
        this.actionService = (ActionService) applicationContext.getBean("ActionService");
        this.transactionService = (TransactionService) applicationContext.getBean("transactionComponent");
        this.siteService = applicationContext.getBean("SiteService", SiteService.class);
        // MNT-10807 : Auditing does not take into account audit.filter.alfresco-access.transaction.user
        UserAuditFilter userAuditFilter = new UserAuditFilter();
        userAuditFilter.setUserFilterPattern("System;.*");
        userAuditFilter.afterPropertiesSet();
        AuditComponent auditComponent = (AuditComponent) applicationContext.getBean("auditComponent");
        auditComponent.setUserAuditFilter(userAuditFilter);
        AuditServiceImpl auditServiceImpl = (AuditServiceImpl) applicationContext.getBean("auditService");
        auditServiceImpl.setAuditComponent(auditComponent);
        this.auditService = (AuditService) applicationContext.getBean("auditService");
        this.scriptService = (ScriptService) applicationContext.getBean("scriptService");
        this.actionTrackingService = (ActionTrackingService) applicationContext.getBean("actionTrackingService");
        this.authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent");

        this.personService = (PersonService) applicationContext.getBean("PersonService");
        this.permissionService = (PermissionService) applicationContext.getBean("PermissionService");
        this.authenticationService = (MutableAuthenticationService) applicationContext.getBean("authenticationService");
        this.nodeRefPropInterceptor = (NodeRefPropertyMethodInterceptor) applicationContext.getBean("nodeRefPropertyInterceptor");

        if (init == false)
        {
            this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

                @Override
                public Void execute() throws Throwable
                {
                    // Authenticate as the system user
                    authenticationComponent.setSystemUserAsCurrentUser();

                    // Create the store and get the root node
                    TaggingServiceImplTest.storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis());
                    TaggingServiceImplTest.rootNode = nodeService.getRootNode(TaggingServiceImplTest.storeRef);

                    // Create the required tagging category
                    NodeRef catContainer = nodeService.createNode(TaggingServiceImplTest.rootNode, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryContainer"), ContentModel.TYPE_CONTAINER).getChildRef();
                    NodeRef catRoot = nodeService.createNode(
                            catContainer,
                            ContentModel.ASSOC_CHILDREN,
                            QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryRoot"),
                            ContentModel.TYPE_CATEGORYROOT).getChildRef();
                    nodeService.createNode(
                            catRoot,
                            ContentModel.ASSOC_CATEGORIES,
                            ContentModel.ASPECT_TAGGABLE,
                            ContentModel.TYPE_CATEGORY).getChildRef();

                    init = true;
                    return null;
                }
            });

        }

        // We want to know when tagging actions have finished running
        asyncOccurs = new AsyncOccurs();
        ((PolicyComponent) applicationContext.getBean("policyComponent")).bindClassBehaviour(
                AsynchronousActionExecutionQueuePolicies.OnAsyncActionExecute.QNAME,
                ActionModel.TYPE_ACTION,
                new JavaBehaviour(asyncOccurs, "onAsyncActionExecute", NotificationFrequency.EVERY_EVENT));

        // We do want action tracking whenever the tag scope updater runs
        UpdateTagScopesActionExecuter updateTagsAction = (UpdateTagScopesActionExecuter) applicationContext.getBean("update-tagscope");
        updateTagsAction.setTrackStatus(true);

        // Create the folders and documents to be tagged
        createTestDocumentsAndFolders();
    }

    @After
    public void tearDown() throws Exception
    {
        removeTestDocumentsAndFolders();
        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
        {
            fail("Test is not transaction-safe.  Fix up transaction handling and re-test.");
        }
    }

    private void createTestDocumentsAndFolders() throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {

                // Authenticate as the system user
                authenticationComponent.setSystemUserAsCurrentUser();

                String guid = GUID.generate();

                // Create a folder
                Map<QName, Serializable> folderProps = new HashMap<QName, Serializable>(1);
                folderProps.put(ContentModel.PROP_NAME, "testFolder" + guid);
                folder = nodeService.createNode(
                        TaggingServiceImplTest.rootNode,
                        ContentModel.ASSOC_CHILDREN,
                        QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testFolder" + guid),
                        ContentModel.TYPE_FOLDER,
                        folderProps).getChildRef();

                // Create a node
                Map<QName, Serializable> docProps = new HashMap<QName, Serializable>(1);
                docProps.put(ContentModel.PROP_NAME, "testDocument" + guid + ".txt");
                document = nodeService.createNode(
                        folder,
                        ContentModel.ASSOC_CONTAINS,
                        QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testDocument" + guid + ".txt"),
                        ContentModel.TYPE_CONTENT,
                        docProps).getChildRef();

                Map<QName, Serializable> props = new HashMap<QName, Serializable>(1);
                props.put(ContentModel.PROP_NAME, "subFolder" + guid);
                subFolder = nodeService.createNode(
                        folder,
                        ContentModel.ASSOC_CONTAINS,
                        QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "subFolder" + guid),
                        ContentModel.TYPE_FOLDER,
                        props).getChildRef();

                props = new HashMap<QName, Serializable>(1);
                props.put(ContentModel.PROP_NAME, "subDocument" + guid + ".txt");
                subDocument = nodeService.createNode(
                        subFolder,
                        ContentModel.ASSOC_CONTAINS,
                        QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "subDocument" + guid + ".txt"),
                        ContentModel.TYPE_CONTENT,
                        props).getChildRef();
                return null;
            }
        });
    }

    private void removeTestDocumentsAndFolders() throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Authenticate as the system user
                authenticationComponent.setSystemUserAsCurrentUser();

                // If anything is a tag scope, stop it being
                NodeRef[] nodes = new NodeRef[]{subDocument, subFolder, document, folder};
                for (NodeRef nodeRef : nodes)
                {
                    if (taggingService.isTagScope(nodeRef))
                    {
                        taggingService.removeTagScope(nodeRef);
                    }
                }

                // Remove the sample nodes
                for (NodeRef nodeRef : nodes)
                {
                    nodeService.deleteNode(nodeRef);
                }

                // Tidy up the audit component, now all the nodes have gone
                auditService.clearAudit(
                        TaggingServiceImpl.TAGGING_AUDIT_APPLICATION_NAME,
                        0l, System.currentTimeMillis() + 1);
                return null;
            }
        });
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test1TagCRUD()
            throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Get the tags
                List<String> tags = taggingService.getTags(TaggingServiceImplTest.storeRef);
                assertNotNull(tags);
                assertEquals(0, tags.size());

                // Create a tag
                taggingService.createTag(TaggingServiceImplTest.storeRef, TAG_1);
                taggingService.createTag(TaggingServiceImplTest.storeRef, UPPER_TAG);

                return null;
            }
        });

        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Get all the tags
                List<String> tags = taggingService.getTags(TaggingServiceImplTest.storeRef);
                assertNotNull(tags);
                assertEquals(2, tags.size());
                assertTrue(tags.contains(TAG_1));
                assertTrue(tags.contains(LOWER_TAG));

                // Get Paged tags with filter
                Pair<List<String>, Integer> pagedTags = taggingService.getPagedTags(TaggingServiceImplTest.storeRef, "one", 0, 10);
                assertNotNull(pagedTags);
                List<String> tagPage = pagedTags.getFirst();
                int allFilteredTagsCount = pagedTags.getSecond();
                assertEquals(1, allFilteredTagsCount);
                assertEquals(1, tagPage.size());
                assertTrue(tagPage.get(0).contains("one"));

                tagPage = taggingService.getTags(TaggingServiceImplTest.storeRef, "one");
                assertNotNull(pagedTags);
                tagPage = pagedTags.getFirst();
                allFilteredTagsCount = pagedTags.getSecond();
                assertEquals(1, allFilteredTagsCount);
                assertEquals(1, tagPage.size());
                assertTrue(tagPage.get(0).contains("one"));

                // Check isTag method
                assertFalse(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_2));
                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_1));
                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, UPPER_TAG));
                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, LOWER_TAG));

                // Remove a tag
                taggingService.deleteTag(TaggingServiceImplTest.storeRef, UPPER_TAG);
                return null;
            }
        });

        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Get all the tags
                List<String> tags = taggingService.getTags(TaggingServiceImplTest.storeRef);
                assertNotNull(tags);
                assertEquals(1, tags.size());
                assertTrue(tags.contains(TAG_1));
                assertFalse(tags.contains(LOWER_TAG));

                // Check isTag method
                assertFalse(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_2));
                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_1));
                assertFalse(taggingService.isTag(TaggingServiceImplTest.storeRef, UPPER_TAG));
                assertFalse(taggingService.isTag(TaggingServiceImplTest.storeRef, LOWER_TAG));
                return null;
            }
        });
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test2AddRemoveTag()
            throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                List<String> tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertTrue(tags.isEmpty());
                assertFalse(taggingService.hasTag(document, TAG_1));

                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_1));
                taggingService.addTag(document, TAG_1);

                tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertEquals(1, tags.size());
                assertTrue(tags.contains(TAG_1));
                assertTrue(taggingService.hasTag(document, TAG_1));

                assertFalse(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_2));
                taggingService.addTag(document, TAG_2);

                assertTrue(taggingService.isTag(TaggingServiceImplTest.storeRef, TAG_2));
                tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertEquals(2, tags.size());
                assertTrue(tags.contains(TAG_1));
                assertTrue(tags.contains(TAG_2));
                assertTrue(taggingService.hasTag(document, TAG_1));
                assertTrue(taggingService.hasTag(document, TAG_2));

                taggingService.removeTag(document, TAG_1);
                tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertEquals(1, tags.size());
                assertFalse(tags.contains(TAG_1));
                assertFalse(taggingService.hasTag(document, TAG_1));
                assertTrue(tags.contains(TAG_2));
                assertTrue(taggingService.hasTag(document, TAG_2));

                List<String> setTags = new ArrayList<String>(2);
                setTags.add(TAG_3);
                setTags.add(TAG_1);
                taggingService.setTags(document, setTags);
                tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertEquals(2, tags.size());
                assertTrue(tags.contains(TAG_1));
                assertFalse(tags.contains(TAG_2));
                assertTrue(tags.contains(TAG_3.toLowerCase()));

                taggingService.clearTags(document);
                tags = taggingService.getTags(document);
                assertNotNull(tags);
                assertTrue(tags.isEmpty());
                return null;
            }
        });
    }

    @Test
    public void test3TagScopeFindAddRemove()
            throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Get scopes for node without
                TagScope tagScope = taggingService.findTagScope(subDocument);
                assertNull(tagScope);
                List<TagScope> tagScopes = taggingService.findAllTagScopes(subDocument);
                assertNotNull(tagScopes);
                assertEquals(0, tagScopes.size());

                // Add scopes
                // TODO should the add return the created scope ??
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);

                // Get the scopes
                tagScope = taggingService.findTagScope(subDocument);
                assertNotNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(subDocument);
                assertNotNull(tagScopes);
                assertEquals(2, tagScopes.size());
                tagScope = taggingService.findTagScope(subFolder);
                assertNotNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(subFolder);
                assertNotNull(tagScopes);
                assertEquals(2, tagScopes.size());
                tagScope = taggingService.findTagScope(folder);
                assertNotNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(folder);
                assertNotNull(tagScopes);
                assertEquals(1, tagScopes.size());

                // Remove a scope
                taggingService.removeTagScope(folder);
                tagScope = taggingService.findTagScope(subDocument);
                assertNotNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(subDocument);
                assertNotNull(tagScopes);
                assertEquals(1, tagScopes.size());
                tagScope = taggingService.findTagScope(subFolder);
                assertNotNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(subFolder);
                assertNotNull(tagScopes);
                assertEquals(1, tagScopes.size());
                tagScope = taggingService.findTagScope(folder);
                assertNull(tagScope);
                tagScopes = taggingService.findAllTagScopes(folder);
                assertNotNull(tagScopes);
                assertEquals(0, tagScopes.size());
                return null;
            }
        });
    }

    @Test
    public void test4TagScope()
            throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // TODO add some tags before the scopes are added

                // Add some tag scopes
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);

                // Add some more tags after the scopes have been added
                taggingService.addTag(subDocument, TAG_1); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_2); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_3); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_1); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_2); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(folder, TAG_2); // folder only
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // re get the tag scopes
                TagScope ts1 = taggingService.findTagScope(subDocument);
                TagScope ts2 = taggingService.findTagScope(folder);

                // Check that the tag scopes got populated
                assertEquals(
                        "Wrong tags on sub folder: " + ts1.getTags(),
                        3, ts1.getTags().size());
                assertEquals(
                        "Wrong tags on main folder: " + ts2.getTags(),
                        3, ts2.getTags().size());

                // check the order and count of the tagscopes
                assertEquals(2, ts1.getTags().get(0).getCount());
                assertEquals(2, ts1.getTags().get(1).getCount());
                assertEquals(1, ts1.getTags().get(2).getCount());
                assertEquals(TAG_1, ts1.getTags().get(0).getName());
                assertEquals(TAG_2, ts1.getTags().get(1).getName());
                assertEquals(TAG_3.toLowerCase(), ts1.getTags().get(2).getName());

                assertEquals(3, ts2.getTags().get(0).getCount());
                assertEquals(2, ts2.getTags().get(1).getCount());
                assertEquals(1, ts2.getTags().get(2).getCount());
                assertEquals(TAG_2, ts2.getTags().get(0).getName());
                assertEquals(TAG_1, ts2.getTags().get(1).getName());
                assertEquals(TAG_3.toLowerCase(), ts2.getTags().get(2).getName());

                // Take some off again
                taggingService.removeTag(folder, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.removeTag(subFolder, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.removeTag(subFolder, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.removeTag(subDocument, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // And add one more
                taggingService.addTag(folder, TAG_3);
                return null;
            }
        });

        /* The following code from the test was commented out as part of REPO-2028 to remove the lucene dependencies in the tests.
         * 
         * @Category({RedundantTests.class,LuceneTests.class}) */
        //
        // this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>()
        // {
        // @Override
        // public Void execute() throws Throwable
        // {
        // // re get the tag scopes
        // TagScope ts1 = taggingService.findTagScope(subDocument);
        // TagScope ts2 = taggingService.findTagScope(folder);
        //
        // // Recheck the tag scopes
        // assertEquals(
        // "Wrong tags on sub folder: " + ts1.getTags(),
        // 2, ts1.getTags().size()
        // );
        // assertEquals(
        // "Wrong tags on main folder: " + ts2.getTags(),
        // 2, ts2.getTags().size()
        // );
        //
        // // Sub-folder should be ordered by tag name, as all values 1
        // assertEquals(1, ts1.getTags().get(0).getCount());
        // assertEquals(1, ts1.getTags().get(1).getCount());
        // assertEquals(TAG_2, ts1.getTags().get(0).getName());
        // assertEquals(TAG_3.toLowerCase(), ts1.getTags().get(1).getName());
        //
        // // Folder should be still sorted by size, as a 2 & a 1
        // assertEquals(2, ts2.getTags().get(0).getCount());
        // assertEquals(1, ts2.getTags().get(1).getCount());
        // assertEquals(TAG_3.toLowerCase(), ts2.getTags().get(0).getName());
        // assertEquals(TAG_2, ts2.getTags().get(1).getName());
        // return null;
        // }
        // });
    }

    @Test
    @SuppressWarnings("unchecked")
    public void test5TagScopeSummary() throws Exception
    {
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // TODO add some tags before the scopes are added

                // Add some tag scopes
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);

                // Add some more tags after the scopes have been added
                taggingService.addTag(subDocument, TAG_1); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_2); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_3); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_1); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_2); // folder+subfolder
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(folder, TAG_2); // folder only
                return null;
            }
        });
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                TagScopePropertyMethodInterceptor.setEnabled(Boolean.TRUE);

                Serializable summaryObj = nodeService.getProperty(folder, ContentModel.PROP_TAGSCOPE_SUMMARY);
                assertTrue("TagScopeSummary value on main folder is not of correct class: " + summaryObj.getClass().getName(),
                        List.class.isAssignableFrom(summaryObj.getClass()));
                assertEquals(3, ((List) summaryObj).size());

                // Check that the next call for the same summary comes from the cache
                Serializable summaryObj2 = nodeService.getProperty(folder, ContentModel.PROP_TAGSCOPE_SUMMARY);
                assertTrue("TagScopeSummary value on main folder did not come from the cache",
                        summaryObj == summaryObj2);

                Map<QName, Serializable> props = nodeService.getProperties(subFolder);
                assertTrue("Properties of subfolder do not include tagScopeSummary", props.containsKey(ContentModel.PROP_TAGSCOPE_SUMMARY));
                summaryObj = props.get(ContentModel.PROP_TAGSCOPE_SUMMARY);
                assertTrue("TagScopeSummary value on subfolder is not of correct class: " + summaryObj.getClass().getName(),
                        List.class.isAssignableFrom(summaryObj.getClass()));
                assertEquals(3, ((List) summaryObj).size());

                TagScopePropertyMethodInterceptor.setEnabled(Boolean.FALSE);

                summaryObj = nodeService.getProperty(folder, ContentModel.PROP_TAGSCOPE_SUMMARY);
                assertNull("TagScopeSummary value on main folder should be null: " + summaryObj,
                        summaryObj);

                props = nodeService.getProperties(subFolder);
                assertFalse("Properties of subfolder should not contain tagScopeProperty", props.containsKey(ContentModel.PROP_TAGSCOPE_SUMMARY));
                return null;
            }
        });
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test6TagScopeRefresh()
            throws Exception
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Add some tags to the nodes
                // tag scope on folder should be ....
                // tag2 = 3
                // tag1 = 2
                // tag3 = 1
                taggingService.addTag(subDocument, TAG_1);
                taggingService.addTag(subDocument, TAG_2);
                taggingService.addTag(subDocument, TAG_3);
                taggingService.addTag(subFolder, TAG_1);
                taggingService.addTag(subFolder, TAG_2);
                taggingService.addTag(folder, TAG_2);

                // Commit the changes. No action will fire, as there
                // aren't currently any tag scopes to be updated!
                return null;
            }
        });
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Add the tag scope
                taggingService.addTagScope(folder);

                // The updates in this case are synchronous, not
                // async, so we don't need to wait for any actions
                return null;
            }
        });
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Get the tag scope and check that all the values have been set correctly
                TagScope tagScope = taggingService.findTagScope(folder);
                assertNotNull(tagScope);
                assertEquals(3, tagScope.getTags().size());
                assertEquals(3, tagScope.getTag(TAG_2).getCount());
                assertEquals(2, tagScope.getTag(TAG_1).getCount());
                assertEquals(1, tagScope.getTag(TAG_3.toLowerCase()).getCount());
                return null;
            }
        });
    }

    @Test
    public void test7TagScopeSetUpdate()
            throws Exception
    {
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Set up tag scope
                taggingService.addTagScope(folder);
                ;

                // Add some tags
                taggingService.addTag(folder, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(document, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(document, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_3);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Check that tag scope
                TagScope ts1 = taggingService.findTagScope(folder);
                assertEquals("Wrong tags on folder tagscope: " + ts1.getTags(), 3, ts1.getTags().size());
                assertEquals(4, ts1.getTag(TAG_1).getCount());
                assertEquals(2, ts1.getTag(TAG_2).getCount());
                assertEquals(1, ts1.getTag(TAG_3.toLowerCase()).getCount());

                // Re-set the tag scopes
                List<String> tags = new ArrayList<String>(3);
                tags.add(TAG_2);
                tags.add(TAG_3);
                tags.add(TAG_4);

                taggingService.setTags(subDocument, tags);
                return null;
            }
        });
        /* This part of the code was commented out as part of the REPO-2028 to remove the lucene dependency from tests
         * 
         * @Category({RedundantTests.class,LuceneTests.class}) */

        // this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>()
        // {
        // @Override
        // public Void execute() throws Throwable
        // {
        // // Check that the tagscope has been updated correctly
        // TagScope ts1 = taggingService.findTagScope(folder);
        // assertEquals(3, ts1.getTag(TAG_1).getCount());
        // assertEquals(2, ts1.getTag(TAG_2).getCount());
        // assertEquals(1, ts1.getTag(TAG_3.toLowerCase()).getCount());
        // assertEquals(1, ts1.getTag(TAG_4).getCount());
        // return null;
        // }
        // });
    }

    /* https://issues.alfresco.com/jira/browse/ETHREEOH-220 */
    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test8ETHREEOH_220() throws Exception
    {
        // Add tag scope to a folder, then add a non-ASCII (unicode)
        // tag onto the folder
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTagScope(folder);
                taggingService.addTag(folder, TAG_I18N);
                return null;
            }
        });

        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Get the tag from the node
                List<String> tags = taggingService.getTags(folder);
                assertNotNull(tags);
                assertEquals(1, tags.size());
                assertEquals(TAG_I18N, tags.get(0));

                // Get the tag from the tagscope
                TagScope tagScope = taggingService.findTagScope(folder);
                assertNotNull(tagScope);
                assertEquals(1, tagScope.getTags().size());
                TagDetails tagDetails = tagScope.getTag(TAG_I18N);
                assertNotNull(tagDetails);
                assertEquals(TAG_I18N, tagDetails.getName());
                assertEquals(1, tagDetails.getCount());
                return null;
            }
        });
    }

    /**
     * Ensures that the tag scope is correctly updated when folders and content are created, updated, moved, copied and deleted.
     */
    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test9TagScopeUpdateViaNodePolicies() throws Exception
    {
        class TestData
        {
            public NodeRef tagFoo1;
            public NodeRef tagFoo2;
            public NodeRef tagFoo3;
            public NodeRef tagBar;
            public NodeRef container1;
            public NodeRef container2;
            public NodeRef taggedFolder;
            public NodeRef taggedFolder2;
            public NodeRef taggedDoc;
            public NodeRef taggedDoc2;
            public NodeRef checkedOutDoc;
        }
        final TestData testData = new TestData();

        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // The various tags we'll be using in testing
                testData.tagFoo1 = taggingService.createTag(folder.getStoreRef(), "Foo1");
                testData.tagFoo2 = taggingService.createTag(folder.getStoreRef(), "Foo2");
                testData.tagFoo3 = taggingService.createTag(folder.getStoreRef(), "Foo3");
                testData.tagBar = taggingService.createTag(folder.getStoreRef(), "Bar");

                List<NodeRef> tagsList = new ArrayList<NodeRef>();

                // Create two containers marked as tag scopes
                Map<QName, Serializable> container1Props = new HashMap<QName, Serializable>(1);
                container1Props.put(ContentModel.PROP_NAME, "Container1");
                testData.container1 = nodeService.createNode(
                        folder,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_FOLDER,
                        container1Props).getChildRef();
                assertEquals(0, nodeService.getChildAssocs(testData.container1).size());

                Map<QName, Serializable> container2Props = new HashMap<QName, Serializable>(1);
                container1Props.put(ContentModel.PROP_NAME, "Container2");
                testData.container2 = nodeService.createNode(
                        folder,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASSOC_CHILDREN,
                        ContentModel.TYPE_FOLDER,
                        container2Props).getChildRef();
                assertEquals(0, nodeService.getChildAssocs(testData.container2).size());

                // Check that the tag scopes are empty
                taggingService.addTagScope(testData.container1);
                taggingService.addTagScope(testData.container2);
                return null;
            }
        });
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertTrue(taggingService.isTagScope(testData.container1));
                assertTrue(taggingService.isTagScope(testData.container2));
                assertEquals(0, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());

                // Create a folder, without any tags on it
                Map<QName, Serializable> taggedFolderProps = new HashMap<QName, Serializable>(1);
                taggedFolderProps.put(ContentModel.PROP_NAME, "Folder");
                testData.taggedFolder = nodeService.createNode(testData.container1, ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_FOLDER, taggedFolderProps).getChildRef();

                // No tags, so no changes should have occured, and no actions
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(0, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());

                // Now update the folder to add in tags
                Map<QName, Serializable> taggedFolderProps = new HashMap<QName, Serializable>(1);
                List<NodeRef> tagsList = new ArrayList<NodeRef>();

                tagsList.add(testData.tagFoo1);
                tagsList.add(testData.tagFoo3);
                taggedFolderProps.put(ContentModel.ASPECT_TAGGABLE, (Serializable) tagsList);
                nodeService.addProperties(testData.taggedFolder, taggedFolderProps);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(
                        "Unexpected tags " + taggingService.findTagScope(testData.container1).getTags(),
                        2, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(
                        "Unexpected tags " + taggingService.findTagScope(testData.container2).getTags(),
                        0, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                // Create a document within it, check that tag scope tags are updated
                List<NodeRef> tagsList = new ArrayList<NodeRef>();
                tagsList.add(testData.tagFoo1);
                tagsList.add(testData.tagFoo2);

                Map<QName, Serializable> taggedDocProps = new HashMap<QName, Serializable>(1);
                taggedDocProps.put(ContentModel.PROP_NAME, "Document");
                taggedDocProps.put(ContentModel.ASPECT_TAGGABLE, (Serializable) tagsList);
                testData.taggedDoc = nodeService.createNode(
                        testData.taggedFolder,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASPECT_TAGGABLE,
                        ContentModel.TYPE_CONTENT,
                        taggedDocProps).getChildRef();
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(2, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                // Check that the Document really is a child of the folder,
                // otherwise later checks will fail for really odd reasons
                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(1, nodeService.getChildAssocs(testData.taggedFolder).size());

                // Check out the node
                // Tags should be doubled up. (We don't care about ContentModel.ASPECT_WORKING_COPY
                // because it isn't applied at suitable times to take not of)
                testData.checkedOutDoc = checkOutCheckInService.checkout(testData.taggedDoc);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(2, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(2, nodeService.getChildAssocs(testData.taggedFolder).size());

                // And check it back in again
                // Tags should go back to how they were
                checkOutCheckInService.checkin(testData.checkedOutDoc, null);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(2, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(1, nodeService.getChildAssocs(testData.taggedFolder).size());

                // Do a node->node copy of the document onto the other container
                Map<QName, Serializable> taggedDocProps = new HashMap<QName, Serializable>(1);
                taggedDocProps.put(ContentModel.PROP_NAME, "CopyDoc");
                assertEquals(0, nodeService.getChildAssocs(testData.container2).size());
                testData.taggedDoc2 = nodeService.createNode(
                        testData.container2,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASPECT_TAGGABLE,
                        ContentModel.TYPE_CONTENT,
                        taggedDocProps).getChildRef();
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(1, nodeService.getChildAssocs(testData.container2).size());
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                copyService.copy(testData.taggedDoc, testData.taggedDoc2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(2, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(2, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());

                // Check that things were fine after the copy
                assertEquals(1, nodeService.getChildAssocs(testData.container2).size());
                assertEquals(testData.container2, nodeService.getPrimaryParent(testData.taggedDoc2).getParentRef());
                assertEquals(testData.container2, taggingService.findTagScope(testData.taggedDoc2).getNodeRef());

                // Copy the folder to another container
                // Does a proper, recursing copy
                testData.taggedFolder2 = copyService.copy(
                        testData.taggedFolder, testData.container2,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASSOC_CHILDREN,
                        true);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(3, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(2, nodeService.getChildAssocs(testData.container2).size());
                assertEquals(1, nodeService.getChildAssocs(testData.taggedFolder2).size());

                assertEquals(2, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(3, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());

                // Update the document on the original
                List<NodeRef> tagsList = new ArrayList<NodeRef>();
                tagsList.add(testData.tagBar);
                Map<QName, Serializable> taggedDocProps = new HashMap<QName, Serializable>(1);
                taggedDocProps.put(ContentModel.ASPECT_TAGGABLE, (Serializable) tagsList);
                taggedDocProps.put(ContentModel.PROP_NAME, "UpdatedDocument");
                nodeService.addProperties(testData.taggedDoc, taggedDocProps);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(3, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(3, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("bar").getCount());

                assertEquals(3, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());

                // Move the document to another container
                testData.taggedDoc = nodeService.moveNode(testData.taggedDoc, testData.container2,
                        ContentModel.ASSOC_CONTAINS,
                        ContentModel.ASPECT_TAGGABLE).getChildRef();
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(2, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(4, taggingService.findTagScope(testData.container2).getTags().size());

                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(3, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("bar").getCount());

                // Check the state of the tree
                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(3, nodeService.getChildAssocs(testData.container2).size());

                assertEquals(testData.container2, nodeService.getPrimaryParent(testData.taggedDoc).getParentRef());
                assertEquals(testData.container2, taggingService.findTagScope(testData.taggedDoc).getNodeRef());
                assertEquals(testData.container2, nodeService.getPrimaryParent(testData.taggedDoc2).getParentRef());
                assertEquals(testData.container2, taggingService.findTagScope(testData.taggedDoc2).getNodeRef());

                // Delete the documents and folder one at a time
                nodeService.deleteNode(testData.taggedDoc); // container 2, "bar"
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(2, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(3, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(2, nodeService.getChildAssocs(testData.container2).size());

                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(3, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());
                assertEquals(null, taggingService.findTagScope(testData.container2).getTag("bar"));

                nodeService.deleteNode(testData.taggedDoc2); // container 2, "foo1", "foo2"
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(2, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(3, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(1, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(1, nodeService.getChildAssocs(testData.container2).size());

                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container1).getTag("foo3").getCount());

                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());
                assertEquals(null, taggingService.findTagScope(testData.container2).getTag("bar"));

                nodeService.deleteNode(testData.taggedFolder); // container 1, "foo1", "foo3"
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(0, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(3, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(0, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(1, nodeService.getChildAssocs(testData.container2).size());

                assertEquals(2, taggingService.findTagScope(testData.container2).getTag("foo1").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo2").getCount());
                assertEquals(1, taggingService.findTagScope(testData.container2).getTag("foo3").getCount());

                nodeService.deleteNode(testData.taggedFolder2); // container 2, has a child also
                return null;
            }
        });
        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                assertEquals(0, taggingService.findTagScope(testData.container1).getTags().size());
                assertEquals(0, taggingService.findTagScope(testData.container2).getTags().size());
                assertEquals(0, nodeService.getChildAssocs(testData.container1).size());
                assertEquals(0, nodeService.getChildAssocs(testData.container2).size());
                return null;
            }
        });
    }

    /**
     * Ensures that a user can only tag a node they can write to, but that the tag scope updates propagate upwards as the system (even to things the user can't write to) Also checks that policies are disabled during tag scope updates, so that the auditable flags aren't incorrectly set by the change
     */
    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test91PermissionsAndPolicies() throws Exception
    {
        class TestData
        {
            public NodeRef taggedNode;
            public NodeRef auditableFolder;
            public Date origModified;
        }
        final TestData testData = new TestData();
        final String USER_1 = "User1";
        authenticationComponent.setSystemUserAsCurrentUser();
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Create a user
                if (authenticationService.authenticationExists(USER_1))
                    authenticationService.deleteAuthentication(USER_1);
                if (personService.personExists(USER_1))
                    personService.deletePerson(USER_1);

                authenticationService.createAuthentication(USER_1, "PWD".toCharArray());
                PropertyMap personProperties = new PropertyMap();
                personProperties.put(ContentModel.PROP_USERNAME, USER_1);
                personProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + USER_1);
                personProperties.put(ContentModel.PROP_FIRSTNAME, "firstName");
                personProperties.put(ContentModel.PROP_LASTNAME, "lastName");
                personProperties.put(ContentModel.PROP_EMAIL, USER_1 + "@example.com");
                personProperties.put(ContentModel.PROP_JOBTITLE, "jobTitle");
                personService.createPerson(personProperties);

                // Give that user permissions on the tagging category root, so
                // they're allowed to add new tags
                NodeRef tn = taggingService.createTag(folder.getStoreRef(), "Testing");
                NodeRef tr = nodeService.getPrimaryParent(tn).getParentRef();
                permissionService.setPermission(tr, USER_1, PermissionService.EDITOR, true);
                permissionService.setPermission(tr, USER_1, PermissionService.CONTRIBUTOR, true);

                // Create a folder with a tag scope on it + auditable aspect
                // User can read but not write
                authenticationComponent.setSystemUserAsCurrentUser();
                testData.auditableFolder = nodeService.createNode(
                        folder, ContentModel.ASSOC_CONTAINS,
                        QName.createQName("Folder"), ContentModel.TYPE_FOLDER).getChildRef();
                nodeService.addAspect(testData.auditableFolder, ContentModel.ASPECT_AUDITABLE, null);
                taggingService.addTagScope(testData.auditableFolder);
                permissionService.setPermission(testData.auditableFolder, USER_1, PermissionService.CONSUMER, true);

                // Auditable checks
                assertEquals("System", nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_CREATOR));
                assertEquals("System", nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_MODIFIER));

                // Create a node without tags, which the user
                // can write to
                testData.taggedNode = nodeService.createNode(
                        testData.auditableFolder, ContentModel.ASSOC_CONTAINS,
                        QName.createQName("Tagged"), ContentModel.TYPE_CONTENT).getChildRef();
                permissionService.setPermission(testData.taggedNode, USER_1, PermissionService.EDITOR, true);

                // Tag the node as the user
                authenticationComponent.setCurrentUser(USER_1);
                assertEquals(0, taggingService.getTags(testData.taggedNode).size());

                nodeService.setProperty(testData.taggedNode, ContentModel.PROP_TITLE, "To ensure we're allowed to write");

                taggingService.addTag(testData.taggedNode, TAG_1);
                taggingService.addTag(testData.taggedNode, TAG_2);
                assertEquals(2, taggingService.getTags(testData.taggedNode).size());

                // Ensure the folder tag scope got the update
                TagScope ts = taggingService.findTagScope(testData.taggedNode);
                assertEquals(testData.auditableFolder, ts.getNodeRef());
                assertEquals(0, ts.getTags().size());

                assertEquals("System", nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_CREATOR));
                assertEquals("System", nodeService.getProperty(ts.getNodeRef(), ContentModel.PROP_MODIFIER));
                return null;
            }
        });
        // Due to timestamp propagation, we need to start a new transaction to get the current folder modified date
        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                testData.origModified = (Date) nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_MODIFIED);
                return null;
            }
        });
        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                TagScope ts = taggingService.findTagScope(testData.taggedNode);
                assertEquals(testData.auditableFolder, ts.getNodeRef());
                assertEquals(2, ts.getTags().size());
                assertEquals(1, ts.getTag(TAG_1).getCount());
                assertEquals(1, ts.getTag(TAG_2).getCount());

                // Ensure the auditable flags on the folder are unchanged
                assertEquals("System", nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_CREATOR));
                assertEquals("System", nodeService.getProperty(ts.getNodeRef(), ContentModel.PROP_MODIFIER));
                assertEquals(testData.origModified.getTime(), ((Date) nodeService.getProperty(testData.auditableFolder, ContentModel.PROP_MODIFIED)).getTime());

                // Tidy up
                authenticationComponent.setSystemUserAsCurrentUser();
                authenticationService.deleteAuthentication(USER_1);
                personService.deletePerson(USER_1);
                return null;
            }
        });
    }

    // == Test the JavaScript API ==
    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test92JSAPI() throws Exception
    {
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                Map<String, Object> model = new HashMap<String, Object>(0);
                model.put("folder", folder);
                model.put("subFolder", subFolder);
                model.put("document", document);
                model.put("subDocument", subDocument);
                model.put("tagScopeTest", false);

                ScriptLocation location = new ClasspathScriptLocation("org/alfresco/repo/tagging/script/test_taggingService.js");
                scriptService.executeScript(location, model);

                // Let the script run
                return null;
            }
        });
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void test93JSTagScope() throws Exception
    {
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Add a load of tags to test the global tag methods with
                taggingService.createTag(storeRef, "alpha");
                taggingService.createTag(storeRef, "alpha double");
                taggingService.createTag(storeRef, "beta");
                taggingService.createTag(storeRef, "gamma");
                taggingService.createTag(storeRef, "delta");

                // Add a load of tags and tag scopes to the object and commit before executing the script
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);

                taggingService.addTag(subDocument, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_3);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subFolder, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(document, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(document, TAG_2);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(folder, TAG_1);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                Map<String, Object> model = new HashMap<String, Object>(0);
                model.put("folder", folder);
                model.put("subFolder", subFolder);
                model.put("document", document);
                model.put("subDocument", subDocument);
                model.put("tagScopeTest", true);
                model.put("store", storeRef.toString());

                ScriptLocation location = new ClasspathScriptLocation(
                        "org/alfresco/repo/tagging/script/test_taggingService.js");
                scriptService.executeScript(location, model);

                // Let the script run
                return null;
            }
        });
    }

    /**
     * Test that the scheduled task will do the right thing when it runs.
     */
    @Test
    public void test93OnStartupJob() throws Exception
    {
        final UpdateTagScopesActionExecuter updateTagsAction = (UpdateTagScopesActionExecuter) applicationContext
                .getBean("update-tagscope");
        class TestData
        {
            public String lockF;
            public String lockSF;
        }
        final TestData testData = new TestData();
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Nothing is pending to start with
                assertEquals(0, updateTagsAction.searchForTagScopesPendingUpdates().size());

                // Take the tag scope lock, so that no real updates will happen
                testData.lockF = updateTagsAction.lockTagScope(folder);
                testData.lockSF = updateTagsAction.lockTagScope(subFolder);

                // Do some tagging
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);

                taggingService.addTag(subDocument, TAG_1);
                taggingService.addTag(subDocument, TAG_2);
                taggingService.addTag(subFolder, TAG_1);
                taggingService.addTag(document, TAG_1);
                taggingService.addTag(folder, TAG_1);
                taggingService.addTag(folder, TAG_3);
                return null;
            }
        });

        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Tag scope updates shouldn't have happened yet,
                // as the scopes are locked
                TagScope ts1 = taggingService.findTagScope(folder);
                TagScope ts2 = taggingService.findTagScope(subFolder);
                assertEquals(
                        "Wrong tags on folder tagscope: " + ts1.getTags(),
                        0, ts1.getTags().size());
                assertEquals(
                        "Wrong tags on folder tagscope: " + ts1.getTags(),
                        0, ts2.getTags().size());

                // Check the pending list now
                assertEquals(2, updateTagsAction.searchForTagScopesPendingUpdates().size());
                List<NodeRef> pendingScopes = updateTagsAction.searchForTagScopesPendingUpdates();
                assertTrue("Not found in " + pendingScopes, pendingScopes.contains(folder));
                assertTrue("Not found in " + pendingScopes, pendingScopes.contains(subFolder));

                // Ensure that we've still got the lock, eg in case
                // of the async execution taking a while to proceed
                updateTagsAction.updateTagScopeLock(folder, testData.lockF);
                updateTagsAction.updateTagScopeLock(subFolder, testData.lockSF);

                // Have the Quartz bean fire now
                // It won't be able to do anything, as the locks are taken
                UpdateTagScopesQuartzJob job = new UpdateTagScopesQuartzJob();
                job.execute(actionService, updateTagsAction);
                return null;
            }
        });
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Check that things are still pending despite the quartz run
                assertEquals(2, updateTagsAction.searchForTagScopesPendingUpdates().size());
                List<NodeRef> pendingScopes = updateTagsAction.searchForTagScopesPendingUpdates();
                assertTrue("Not found in " + pendingScopes, pendingScopes.contains(folder));
                assertTrue("Not found in " + pendingScopes, pendingScopes.contains(subFolder));
                return null;
            }
        });

        // Give back our locks, so we can proceed
        updateTagsAction.unlockTagScope(folder, testData.lockF);
        updateTagsAction.unlockTagScope(subFolder, testData.lockSF);

        // Fire off the quartz bean, this time it can really work
        UpdateTagScopesQuartzJob job = new UpdateTagScopesQuartzJob();
        job.execute(actionService, updateTagsAction);

        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Now check again - nothing should be pending
                assertEquals(0, updateTagsAction.searchForTagScopesPendingUpdates().size());

                TagScope ts1 = taggingService.findTagScope(folder);
                TagScope ts2 = taggingService.findTagScope(subFolder);
                assertEquals(
                        "Wrong tags on folder tagscope: " + ts1.getTags(),
                        3, ts1.getTags().size());
                assertEquals(
                        "Wrong tags on folder tagscope: " + ts1.getTags(),
                        2, ts2.getTags().size());

                assertEquals(4, ts1.getTag(TAG_1).getCount());
                assertEquals(1, ts1.getTag(TAG_2).getCount());
                assertEquals(1, ts1.getTag(TAG_3.toLowerCase()).getCount());

                assertEquals(2, ts2.getTag(TAG_1).getCount());
                assertEquals(1, ts2.getTag(TAG_2).getCount());
                return null;
            }
        });
    }

    /**
     * Test that when multiple threads do tag updates, the right thing still happens
     */
    @Test
    public void test94MultiThreaded() throws Exception
    {
        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTagScope(folder);
                taggingService.addTagScope(subFolder);
                return null;
            }
        });

        // Reset the action count
        asyncOccurs.wantedActionsCount = 0;

        // Prepare a bunch of threads to do tagging
        final List<Thread> threads = new ArrayList<Thread>();
        final String[] tags = new String[]{TAG_1, TAG_2, TAG_3, TAG_4, TAG_5,
                "testTag06", "testTag07", "testTag08", "testTag09", "testTag10",
                "testTag11", "testTag12", "testTag13", "testTag14", "testTag15",
                "testTag16", "testTag17", "testTag18", "testTag19", "testTag20"};
        for (String tmpTag : tags)
        {
            final String tag = tmpTag;
            Thread t = new Thread(new Runnable() {
                @Override
                public synchronized void run()
                {
                    // Let everything catch up
                    try
                    {
                        wait();
                    }
                    catch (InterruptedException e)
                    {}
                    logger.debug(Thread.currentThread() + " - About to start tagging for " + tag);

                    // Do the updates
                    AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName());
                    RetryingTransactionCallback<Void> txnCallback = new RetryingTransactionCallback<Void>() {
                        @Override
                        public Void execute() throws Throwable
                        {
                            taggingService.addTag(folder, tag);
                            taggingService.addTag(subFolder, tag);
                            taggingService.addTag(subDocument, tag);
                            logger.debug(Thread.currentThread() + " - Tagging for " + tag);
                            return null;
                        }
                    };
                    try
                    {
                        transactionService.getRetryingTransactionHelper().doInTransaction(txnCallback);
                    }
                    catch (Throwable e)
                    {
                        logger.error("Tagging failed: " + e);
                    }
                    logger.debug(Thread.currentThread() + " - Done tagging for " + tag);

                    // Wait briefly for thing to catch up, before we
                    // declare ourselves to be done
                    try
                    {
                        Thread.sleep(150);
                    }
                    catch (InterruptedException e)
                    {}
                }
            });
            threads.add(t);
            t.start();
        }

        // Release the threads
        logger.info("Releasing tagging threads");
        for (Thread t : threads)
        {
            t.interrupt();
        }

        // Wait for the threads to finish (and they will finish)
        // The threads will generate further asynchronous actions
        for (Thread t : threads)
        {
            t.join();
        }
        logger.info("All threads should have finished");

        // Have a brief pause, while we wait for their related
        // async actions to kick off
        Thread.sleep(150);

        // Wait until we've had 20 async tagging actions run (One per Thread)
        // Not all of the actions will do something, but we just need to
        // wait for all of them!
        // As a backup check, also ensure that the action tracking service
        // shows none of them running either
        for (int i = 0; i < 600; i++)
        {
            try
            {
                if (asyncOccurs.wantedActionsCount < tags.length)
                {
                    if (i % 50 == 0)
                    {
                        logger.info("Done " + asyncOccurs.wantedActionsCount + " of " + tags.length);
                    }
                    Thread.sleep(100);
                    continue;
                }
                if (actionTrackingService.getAllExecutingActions().size() > 0)
                {
                    if (i % 50 == 0)
                    {
                        List<ExecutionSummary> actions = actionTrackingService.getAllExecutingActions();
                        logger.info("Waiting on " + actions.size() + " actions: " + actions);
                    }
                    Thread.sleep(100);
                    continue;
                }
                break;
            }
            catch (InterruptedException e)
            {}
        }

        // Extra sleep just to be sure things are quiet before we continue
        // (Allows anything that runs after the async actions commit to
        // finish up for example)
        Thread.sleep(175);
        System.out.println("Done waiting for tagging, now checking");

        // Now check that things ended up as planned
        transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                TagScope ts1 = taggingService.findTagScope(folder);
                TagScope ts2 = taggingService.findTagScope(subFolder);
                assertEquals(
                        "Wrong tags on folder tagscope: " + ts1.getTags(),
                        tags.length, ts1.getTags().size());
                assertEquals(
                        "Wrong tags on subfolder tagscope: " + ts2.getTags(),
                        tags.length, ts2.getTags().size());

                // Each tag should crop up 3 times on the folder
                // and twice for the subfolder
                for (String tag : tags)
                {
                    assertEquals(3, ts1.getTag(tag.toLowerCase()).getCount());
                    assertEquals(2, ts2.getTag(tag.toLowerCase()).getCount());
                }

                // All done
                return null;
            }
        });
    }

    public class AsyncOccurs implements OnAsyncActionExecute
    {
        private Object waitForExecutionLock = new Object();
        private static final long waitTime = 3500;
        private static final String ACTION_TYPE = UpdateTagScopesActionExecuter.NAME;

        private int wantedActionsCount = 0;

        @Override
        public void onAsyncActionExecute(Action action, NodeRef actionedUponNodeRef)
        {
            if (action.getActionDefinitionName().equals(ACTION_TYPE))
            {
                wantedActionsCount++;
                synchronized (waitForExecutionLock)
                {
                    try
                    {
                        waitForExecutionLock.notify();
                    }
                    catch (IllegalMonitorStateException e)
                    {}
                }
            }
            else
            {
                System.out.println("Ignoring unexpected async action:" + action);
            }
        }

        public <T> T awaitExecution(RetryingTransactionCallback<T> callback) throws Exception
        {
            T returnVal = transactionService.getRetryingTransactionHelper().doInTransaction(callback);

            synchronized (waitForExecutionLock)
            {

                // Always wait 100ms
                waitForExecutionLock.wait(100);

                // If there are actions if the required type
                // currently running, keep waiting longer for
                // them to finish
                if (actionTrackingService.getExecutingActions(ACTION_TYPE).size() > 0)
                {
                    long now = System.currentTimeMillis();
                    waitForExecutionLock.wait(waitTime);

                    if (System.currentTimeMillis() - now >= waitTime)
                    {
                        System.err.println("Warning - trigger wasn't received");
                    }
                }
            }

            // If there are any more actions of the same type,
            // then wait for them to finish too
            for (int i = 0; i < 100; i++)
            {
                if (actionTrackingService.getExecutingActions(ACTION_TYPE).size() == 0)
                {
                    break;
                }
                try
                {
                    Thread.sleep(100);
                }
                catch (InterruptedException e)
                {}
            }
            if (actionTrackingService.getExecutingActions(ACTION_TYPE).size() > 0)
            {
                System.err.println("Warning - not all actions finished");
            }

            return returnVal;
        }
    }

    /**
     * Test for https://https://issues.alfresco.com/jira/browse/ALF-17260.
     *
     * When the audit queue for the tagging service contains more than 100 entries which aren't update, {@link UpdateTagScopesActionExecuter} was failing to update the tag scope of containers correctly.
     *
     * @throws Exception
     */
    @Test
    public void testALF_17260() throws Exception
    {
        // Add tag scope to our container
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @Override
            public Void execute() throws Throwable
            {
                // Add some tag scopes
                taggingService.addTagScope(folder);
                return null;
            }
        });

        // Generate ~1800 audit entries, none of which are updates, as determined by UpdateTagScopesActionExecuter
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                for (int j = 0; j < 10; j++)
                {
                    final int k = j;
                    RetryingTransactionCallback<NodeRef> createFoldersAndTags = new RetryingTransactionCallback<NodeRef>() {
                        @Override
                        public NodeRef execute() throws Throwable
                        {
                            NodeRef folder1 = fileFolderService.create(folder, "Test1", ContentModel.TYPE_FOLDER).getNodeRef();
                            NodeRef folder2 = fileFolderService.create(folder, "Test2", ContentModel.TYPE_FOLDER).getNodeRef();

                            for (int i = 0; i < 102; i++)
                            {
                                NodeRef file = fileFolderService.create(folder1, k + "_test_" + i + ".text", ContentModel.TYPE_CONTENT).getNodeRef();
                                taggingService.addTag(file, k + "testTag" + i);
                            }

                            List<ChildAssociationRef> childRefs = nodeService.getChildAssocs(folder1);
                            for (ChildAssociationRef childRef : childRefs)
                            {
                                taggingService.clearTags(childRef.getChildRef());
                            }
                            QName assocQName = nodeService.getPrimaryParent(folder1).getTypeQName();
                            nodeService.moveNode(folder1, folder2, ContentModel.ASSOC_CONTAINS, assocQName);
                            return folder2;
                        }
                    };

                    NodeRef folder2 = transactionService.getRetryingTransactionHelper().doInTransaction(createFoldersAndTags);
                    nodeService.deleteNode(folder2);
                }

                return null;
            }
        });

        // The tag scope of our container should be empty at this stage.
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // re get the tag scopes
                TagScope ts1 = taggingService.findTagScope(folder);

                // Check that the tag scopes got populated
                assertEquals("Wrong tags on sub folder: " + ts1.getTags(), 0, ts1.getTags().size());
                return null;
            }
        });

        // Add some tags which should update the tag scope.
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                taggingService.addTag(subDocument, TAG_1); // folder+subfolder
                return null;
            }
        });

        // Prior to fixing ALF-17260, the tag scope of our container wasn't being updated correctly. These
        // assertions were failing.
        asyncOccurs.awaitExecution(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // re get the tag scopes
                TagScope ts1 = taggingService.findTagScope(folder);

                // Check that the tag scopes got populated
                assertEquals("Wrong tags on folder: " + ts1.getTags(), 1, ts1.getTags().size());
                assertEquals("Wrong tag name", TAG_1, ts1.getTags().get(0).getName());
                assertEquals("Wrong number of documents", 1, ts1.getTags().get(0).getCount());
                return null;
            }
        });
    }

    @Test
    public void testTagFileRead() throws UnsupportedEncodingException
    {
        List<TagDetails> tags = TaggingServiceImpl.readTagDetails(new ByteArrayInputStream("Tag1|10\nTag2|20\nInvalid\nInvalid2|\nInvalid3|One\nTooMany|1|2\n\n".getBytes("UTF-8")));
        assertEquals(3, tags.size());
        assertEquals(tags.get(0).getName(), "Tag1");
        assertEquals(tags.get(1).getName(), "Tag2");
        assertEquals(tags.get(2).getName(), "TooMany");
        assertEquals(tags.get(0).getCount(), 10);
        assertEquals(tags.get(1).getCount(), 20);
        assertEquals(tags.get(2).getCount(), 1);
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void testPagedTags() throws UnsupportedEncodingException
    {
        authenticationComponent.setSystemUserAsCurrentUser();
        Pair<List<String>, Integer> tags = taggingService.getPagedTags(storeRef, 1, 10);
        assertNotNull(tags);
        PagingResults<Pair<NodeRef, String>> res = taggingService.getTags(storeRef, new PagingRequest(10));
        assertNotNull(res);

        String guid = GUID.generate();
        // Create a node
        Map<QName, Serializable> docProps = new HashMap<QName, Serializable>(1);
        String docName = "testDocument" + guid + ".txt";
        docProps.put(ContentModel.PROP_NAME, docName);
        NodeRef myDoc = nodeService.createNode(
                folder,
                ContentModel.ASSOC_CONTAINS,
                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, docName),
                ContentModel.TYPE_CONTENT,
                docProps).getChildRef();
        taggingService.addTag(myDoc, "dog");

        res = taggingService.getTags(myDoc, new PagingRequest(10));
        assertNotNull(res);
        assertTrue(res.getTotalResultCount().getFirst() == 1);
    }

    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void testChangeTags() throws UnsupportedEncodingException
    {
        try
        {
            taggingService.changeTag(storeRef, null, null);
            fail("Should throw exception");
        }
        catch (TaggingException tae)
        {
            tae.getMessage().contains("Existing tag cannot be null");
        }

        try
        {
            taggingService.changeTag(storeRef, "bob", null);
            fail("Should throw exception");
        }
        catch (TaggingException tae)
        {
            tae.getMessage().contains("New tag cannot be null");
        }

        try
        {
            taggingService.changeTag(storeRef, "bob", "bob");
            fail("Should throw exception");
        }
        catch (TaggingException tae)
        {
            tae.getMessage().contains("New and existing tags are the same");
        }

        try
        {
            taggingService.changeTag(storeRef, "bob", "hope");
            fail("Should throw exception");
        }
        catch (NonExistentTagException net)
        {
            net.getMessage().contains("not found");
        }

        try
        {
            List<String> storeTags = taggingService.getTags(storeRef);
            assertNotNull(storeTags);
            assertTrue(storeTags.size() > 1);
            taggingService.changeTag(storeRef, storeTags.get(0), storeTags.get(1));
            fail("Should throw exception");
        }
        catch (TagExistsException tee)
        {
            tee.getMessage().contains("already exists");
        }
    }

    /* Test adding tags containing \n and | chars. Test all ways to create tag (e.g. createTag, addTag, setTags) */
    @Test
    public void testBadTags()
    {
        testTag(BAD_TAG);
        testTag(BAD_TAG2);
    }

    private void testTag(final String tag)
    {
        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                try
                {
                    taggingService.createTag(storeRef, tag);
                    fail();
                }
                catch (IllegalArgumentException iae)
                {
                    //
                }

                try
                {
                    taggingService.addTag(document, tag);
                    fail();
                }
                catch (IllegalArgumentException iae)
                {
                    //
                }

                try
                {
                    List<String> setTags = new ArrayList<String>(2);
                    setTags.add(tag);
                    taggingService.setTags(document, setTags);

                    fail();
                }
                catch (IllegalArgumentException iae)
                {
                    //
                }

                return null;
            }
        });
    }

    /**
     * Tests that when the deleteTag() method runs, it will remove invalid references to the deleted tag.
     */
    @Test
    @Category({RedundantTests.class, LuceneTests.class})
    public void testDeleteTag() throws Exception
    {

        try
        {
            // We instruct the 'nodeRefPropInterceptor' to skip processing on the 'get' methods.
            // This is needed because this interceptor removes any properties which are invalid. e.g. have been deleted.
            // We need to make sure that the 'taggable' property stays put.
            nodeRefPropInterceptor.setFilterOnGet(false);

            this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

                @SuppressWarnings("unchecked")
                @Override
                public Void execute() throws Throwable
                {
                    taggingService.clearTags(folder);
                    // addTag uses lucene to get a reference to the existing TAG_1 tag node.
                    // this fails without lucene
                    taggingService.addTag(folder, TAG_1);

                    // The deleteTag() should remove any reference to the deleted tag
                    List<NodeRef> taggableProperty = (List<NodeRef>) nodeService.getProperty(folder, ContentModel.PROP_TAGS);
                    assertTrue("Our folder should have a reference on one tag.", taggableProperty.size() == 1);

                    taggingService.deleteTag(TaggingServiceImplTest.storeRef, TAG_1);

                    // The deleteTag() should remove any reference to the deleted tag
                    taggableProperty = (List<NodeRef>) nodeService.getProperty(folder, ContentModel.PROP_TAGS);
                    assertTrue("Our folder shouldn't have any references left to deleted tags.", taggableProperty.size() == 0);

                    return null;
                }
            });
        }
        finally
        {
            nodeRefPropInterceptor.setFilterOnGet(true);
        }
    }

    /**
     * ALF-21875
     */
    @Test
    public void testTagScopeIfUrlNull()
    {

        TagScopePropertyMethodInterceptor.setEnabled(Boolean.TRUE);
        AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
        String siteName = GUID.generate();

        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @SuppressWarnings("unchecked")
            @Override
            public Void execute() throws Throwable
            {

                // STEP1 CreateSite
                SiteInfo siteInfo = createSite(siteName, "doclib", SiteVisibility.PUBLIC);

                // STEP2 upload a document in documentLibrary
                NodeRef documentLibraryOfSite = siteService.getContainer(siteInfo.getShortName(), "doclib");

                NodeRef containerTagScope = fileFolderService
                        .create(documentLibraryOfSite, "containerTagScope" + GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef();

                // STEP4 create tag
                String tagName = GUID.generate();
                taggingService.createTag(storeRef, tagName);

                // Add some tag scopes
                taggingService.addTagScope(containerTagScope);

                NodeRef file = fileFolderService.create(containerTagScope, "_test_" + GUID.generate() + ".text", ContentModel.TYPE_CONTENT)
                        .getNodeRef();
                taggingService.addTag(file, tagName);
                fileFolderService.delete(file);

                return null;
            }
        });

        this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

            @SuppressWarnings("unchecked")
            @Override
            public Void execute() throws Throwable
            {

                // STEP5 start job taggingStartupJobDetail
                // Fire off the quartz bean, this time it can really work
                final UpdateTagScopesActionExecuter updateTagsAction = (UpdateTagScopesActionExecuter) applicationContext.getBean("update-tagscope");
                UpdateTagScopesQuartzJob job = new UpdateTagScopesQuartzJob();
                job.execute(actionService, updateTagsAction);

                return null;
            }
        });

        // STEP6 execute script
        // Create a model to pass to the unit test scripts
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("customSiteName", siteName);

        // Execute the unit test script
        ScriptLocation location = new ClasspathScriptLocation("org/alfresco/repo/site/script/test_tagScopeALF_21875.js");
        this.scriptService.executeScript(location, model);

        TagScopePropertyMethodInterceptor.setEnabled(Boolean.FALSE);

    }

    private SiteInfo createSite(String siteShortName, String componentId, SiteVisibility visibility)
    {
        // Create a public site
        SiteInfo siteInfo = this.siteService.createSite(TEST_SITE_PRESET, siteShortName, TEST_TITLE, TEST_DESCRIPTION, visibility);
        this.siteService.createContainer(siteShortName, componentId, ContentModel.TYPE_FOLDER, null);
        return siteInfo;
    }

}
